using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using Be.Timvw.Framework.Domain.Generators;
using Be.Timvw.Framework.Domain.Ranges;

namespace Be.Timvw.Framework.Domain.Effectivity
{
    /// <summary>
    /// Represents an element that changes over time. 
    /// It consists out of effectivities of that element that are effective in different periods.
    /// </summary>
    public class Temporal<T> : ITemporal<T>
    {
        private const string NoElementEffectiveOnMessage = "No element is effective on {0}";

        private readonly IList<IEffectivity<T>> effectivities;
        private readonly IDiscreteValuesGenerator<DateTime> generator;

        /// <summary>
        /// Initializes a new instance of the <see cref="Temporal&lt;T&gt;"/> class.
        /// </summary>
        public Temporal()
        : this(new SecondsGenerator())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Temporal&lt;T&gt;"/> class.
        /// </summary>
        public Temporal(IDiscreteValuesGenerator<DateTime> generator)
        : this(new List<IEffectivity<T>>(), generator)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Temporal&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="effectivities">The effectivities.</param>
        /// <param name="generator">The generator.</param>
        protected internal Temporal(IEnumerable<IEffectivity<T>> effectivities, IDiscreteValuesGenerator<DateTime> generator)
        {
            this.effectivities = new List<IEffectivity<T>>(effectivities);
            this.generator = generator;
        }

        /// <summary>
        /// Gets the effectivities.
        /// </summary>
        /// <value>The effectivities.</value>
        public IList<IEffectivity<T>> Effectivities
        {
            get { return new ReadOnlyCollection<IEffectivity<T>>(this.effectivities); }
        }

        #region ITemporal<T> Members

        /// <summary>
        /// Adds a version of the element, that is effective from the given date.
        /// </summary>
        /// <param name="element"></param>
        /// <param name="from"></param>
        public void Modify(T element, DateTime from)
        {
            // the first effectivity is valid till end of time.
            if(this.effectivities.Count == 0)
            {
                Range<DateTime> validityPeriod = new Range<DateTime>(from, this.generator.MaxValue);
                IEffectivity<T> effectivity = new Effectivity<T>(element, validityPeriod);
                this.effectivities.Add(effectivity);
                return;
            }

            IEffectivity<T> snapshot;
            if(!this.TryGetSnapshot(from, out snapshot))
            {
                // the from period is before all other effectivities, 
                // thus it will be valid untill first already existing effectivity.
                IEffectivity<T> firstEffectivity = this.effectivities[0];
                Range<DateTime> validityPeriod = new Range<DateTime>(from, this.generator.GeneratePreviousValue(firstEffectivity.Period.Begin));
                IEffectivity<T> effectivity = new Effectivity<T>(element, validityPeriod);
                this.effectivities.Insert(0, effectivity);
                return;
            }
            else
            {
                // the from period is in an already existing effectivity, 
                // thus the new effectivity will be valid untill the end of this effectivity
                // and the already existing effectivity will be shortened untill the from date.
                Range<DateTime> validityPeriod = new Range<DateTime>(from, snapshot.Period.End);
                IEffectivity<T> effectivity = new Effectivity<T>(element, validityPeriod);

                int snapshotIndex = this.effectivities.IndexOf(snapshot);
                this.effectivities.Insert(snapshotIndex + 1, effectivity);

                // if the end of the already existing effectivity would be modified to somewhere before it's begin,
                // the already existing effectivity will be removed.
                if(snapshot.Period.Begin < this.generator.GeneratePreviousValue(effectivity.Period.Begin))
                {
                    this.effectivities[snapshotIndex] = new Effectivity<T>(snapshot.Element,
                    new Range<DateTime>(snapshot.Period.Begin, this.generator.GeneratePreviousValue(from)));
                }
                else
                {
                    this.effectivities.Remove(snapshot);
                }
            }
        }

        /// <summary>
        /// Gets the effectivity that is valid on the given date.
        /// </summary>
        /// <param name="validityDate">The validity date.</param>
        /// <returns></returns>
        public IEffectivity<T> GetSnapshot(DateTime validityDate)
        {
            IEffectivity<T> effectivity;
            if(this.TryGetSnapshot(validityDate, out effectivity))
            {
                return effectivity;
            }

            throw new ArgumentOutOfRangeException(string.Format(CultureInfo.InvariantCulture,
            NoElementEffectiveOnMessage, validityDate));
        }

        /// <summary>
        /// Tries to get the effectivity that is valid on the given date.
        /// </summary>
        /// <param name="validityDate"></param>
        /// <param name="effectivity"></param>
        /// <returns></returns>
        public bool TryGetSnapshot(DateTime validityDate, out IEffectivity<T> effectivity)
        {
            foreach(IEffectivity<T> anEffectivity in this.effectivities)
            {
                if(anEffectivity.IsEffectiveOn(validityDate))
                {
                    effectivity = anEffectivity;
                    return true;
                }
            }

            effectivity = default(IEffectivity<T>);
            return false;
        }

        #endregion
    }
}